﻿using System;
using System.IdentityModel.Tokens;
using System.Net;
using System.Security.Cryptography;
using System.ServiceModel.Security;
using System.ServiceModel.Web;
using System.Text;
using System.Xml;
using Microsoft.IdentityModel.Claims;
using Microsoft.IdentityModel.Tokens;

namespace Microsoft.IdentityModel.OAuth
{

    public class SWTSecurityTokenHandler : SecurityTokenHandler
    {
        public const string TokenTypeIdentifier = "http://schemas.microsoft.com/ws/2010/07/identitymodel/tokens/SWT";
        public override string[] GetTokenTypeIdentifiers()
        {
            return new string[] { TokenTypeIdentifier };
        }

        public override Type TokenType
        {
            get { return typeof(SimpleWebToken); }
        }

        public override bool CanReadToken(XmlReader reader)
        {
            return reader.IsStartElement("BinarySecurityToken", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd") &&
                reader.GetAttribute("ValueType") == "http://schemas.xmlsoap.org/ws/2009/11/swt-token-profile-1.0";
        }

        public override SecurityToken ReadToken(XmlReader reader)
        {
            if (!this.CanReadToken(reader))
                throw new InvalidOperationException("Handler cannot read this token.");

            var swtBuffer = Convert.FromBase64String(reader.ReadElementContentAsString());
            var swt = Encoding.Default.GetString(swtBuffer);
            return new SimpleWebToken(swt);
        }

        public override bool CanValidateToken
        {
            get
            {
                return true;
            }
        }
        public override ClaimsIdentityCollection ValidateToken(SecurityToken token)
        {
            var swt = token as SimpleWebToken;

            if (swt == null)
                throw new InvalidOperationException("Token must be of type SimpleWebToken"); 

            InMemorySymmetricSecurityKey securityKey = null;
            try
            {
                securityKey = (InMemorySymmetricSecurityKey)base.Configuration.IssuerTokenResolver.ResolveSecurityKey(new KeyNameIdentifierClause(swt.Issuer));
            }
            catch (InvalidOperationException)
            {
                throw new WebFaultException<HttpError>(
                    new HttpError { Code = HttpStatusCode.Unauthorized, Description = "Unable to resovle key required for signature varification." }, HttpStatusCode.Unauthorized);
            }

            TokenValidator validator = new TokenValidator(securityKey.GetSymmetricKey());

            if (!validator.IsHMACValid(swt.RawToken))
                throw new WebFaultException<HttpError>(
                    new HttpError { Code = HttpStatusCode.Unauthorized, Description = "Failed to verify the signature of the token." }, HttpStatusCode.Unauthorized);

            //TODO: incorporate clock skew in expiry calculation.
            //if (validator.IsExpired(swt.Token))
            //    throw new WebFaultException<HttpError>(
            //        new HttpError { Code = HttpStatusCode.Unauthorized, Description = "Token is expired." }, HttpStatusCode.Unauthorized);

            if (base.Configuration.IssuerNameRegistry != null)
                if (base.Configuration.IssuerNameRegistry.GetIssuerName(token) != swt.Issuer)
                    throw new WebFaultException<HttpError>(
                        new HttpError { Code = HttpStatusCode.Unauthorized, Description = "This issuer is not trusted" }, HttpStatusCode.Unauthorized);

            if (base.Configuration.AudienceRestriction.AudienceMode == System.IdentityModel.Selectors.AudienceUriMode.Always)
            {
                //enforce audience restriction....
                bool valid = base.Configuration.AudienceRestriction.AllowedAudienceUris.Contains(new Uri(swt.Audience));
                if (!valid)
                    throw new WebFaultException<HttpError>(
                        new HttpError { Code = HttpStatusCode.Unauthorized, Description = "Token doesn't meet audience restrictions." }, HttpStatusCode.Unauthorized);
            }

            var incomingIdentity = new ClaimsIdentity(swt.Claims, "OAUTH-SWT");

            return new ClaimsIdentityCollection(new IClaimsIdentity[] { incomingIdentity });
        }

        public override SecurityToken CreateToken(SecurityTokenDescriptor tokenDescriptor)
        {
            var sb = new StringBuilder();
            foreach (var c in tokenDescriptor.Subject.Claims)
                sb.AppendFormat("{0}={1}&", c.ClaimType.UrlEncode(), c.Value.UrlEncode());
            sb.AppendFormat("Issuer={0}&", tokenDescriptor.TokenIssuerName.UrlEncode());
            sb.AppendFormat("Audience={0}&", tokenDescriptor.AppliesToAddress.UrlEncode());

            var seconds = (tokenDescriptor.Lifetime.Expires - tokenDescriptor.Lifetime.Created);

            double lifeTimeInSeconds = 3600;
            if (seconds.HasValue)
                lifeTimeInSeconds = seconds.Value.TotalSeconds;

            sb.AppendFormat("ExpiresOn={0:0}", (DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds + lifeTimeInSeconds);

            var unsignedToken = sb.ToString();

            var key = (InMemorySymmetricSecurityKey)tokenDescriptor.SigningCredentials.SigningKey;
            HMACSHA256 hmac = new HMACSHA256(key.GetSymmetricKey());
            var sig = hmac.ComputeHash(Encoding.ASCII.GetBytes(unsignedToken));

            string signedToken = String.Format("{0}&HMACSHA256={1}",
                unsignedToken,
                Convert.ToBase64String(sig).UrlEncode());

            return new SimpleWebToken(signedToken);
        }

        public override SecurityKeyIdentifierClause CreateSecurityTokenReference(SecurityToken token, bool attached)
        {
            var swt = token as SimpleWebToken;
            if (swt == null)
                throw new InvalidOperationException("Token must be of type SimpleWebToken");

            return new KeyNameIdentifierClause(swt.Issuer);
        }
    }
}
